using System.Collections.Generic;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Framework.Internal;
using Castle.Components.Binder;
using NHibernate;
using NHibernate.Criterion;

#region license

// Copyright 2004-2008 Castle Project - http://www.castleproject.org/
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

namespace System.Web.Mvc.ActiveRecordSupport {
    /// <summary>
    /// Class responsible on loading records for parameters marked with the <see cref="ARFetchAttribute" />.
    /// </summary>
    public class ARFetcher : IModelBinder {
        private readonly IConverter converter;
        private readonly ARFetchAttribute attr;

        public ARFetcher(IConverter converter, ARFetchAttribute attr) {
            this.converter = converter;
            this.attr = attr;
        }

        public object FetchActiveRecord(Type paramType, string paramName,
                                        HttpRequestBase request,
                                        IDictionary<string, object> customActionParameters) {
            bool isArray = paramType.IsArray;

            if (isArray)
                paramType = paramType.GetElementType();

            ActiveRecordModel model = ActiveRecordModel.GetModel(paramType);

            if (model == null) {
                throw new ApplicationException(String.Format("'{0}' is not an ActiveRecord " +
                                                             "class. It could not be bound to an [ARFetch] attribute.", paramType.Name));
            }

            if (model.CompositeKey != null) {
                throw new ApplicationException("ARFetch only supports single-attribute primary keys");
            }

            String webParamName = attr.RequestParameterName ?? paramName;

            if (!isArray) {
                string value = GetParameterValue(webParamName, customActionParameters, request);
                return LoadActiveRecord(paramType, value, model);
            }

            object[] pks = GetParameterValues(webParamName, customActionParameters, request);

            Array objs = Array.CreateInstance(paramType, pks.Length);

            for (int i = 0; i < objs.Length; i++) {
                objs.SetValue(LoadActiveRecord(paramType, pks[i], model), i);
            }

            return objs;
        }

        private object[] GetParameterValues(string webParamName, IDictionary<string, object> customActionParameters, HttpRequestBase request) {
            object tmp;
            object[] pks;
            if (customActionParameters.TryGetValue(webParamName, out tmp) == false || (tmp is Array) == false)
                pks = request.Params.GetValues(webParamName);
            else
                pks = (object[]) tmp;

            if (pks == null) {
                pks = new object[0];
            }
            return pks;
        }

        private string GetParameterValue(string webParamName, IDictionary<string, object> customActionParameters, HttpRequestBase request) {
            string value;
            object tmp;
            if (customActionParameters.TryGetValue(webParamName, out tmp) == false || tmp == null)
                value = request.Params[webParamName];
            else
                value = tmp.ToString();
            return value;
        }

        private object LoadActiveRecord(Type type, object pk, ActiveRecordModel model) {
            object instance = null;

            if (pk != null && !String.Empty.Equals(pk)) {
                PrimaryKeyModel pkModel = ObtainPrimaryKey(model);

                Type pkType = pkModel.Property.PropertyType;

                bool conversionSucceeded;
                object convertedPk = converter.Convert(pkType, pk.GetType(), pk, out conversionSucceeded);

                if (!conversionSucceeded) {
                    throw new ApplicationException(string.Format("ARFetcher could not convert PK {0} to type {1}", pk, pkType));
                }

                if (string.IsNullOrEmpty(attr.Eager)) {
                    // simple load
                    instance = ActiveRecordMediator.FindByPrimaryKey(type, convertedPk, attr.Required);
                } else {
                    // load using eager fetching of lazy collections
                    DetachedCriteria criteria = DetachedCriteria.For(type);
                    criteria.Add(Restrictions.Eq(pkModel.Property.Name, convertedPk));
                    foreach (var associationToEagerFetch in attr.Eager.Split(',')) {
                        string clean = associationToEagerFetch.Trim();
                        if (clean.Length == 0) {
                            continue;
                        }

                        criteria.SetFetchMode(clean, FetchMode.Eager);
                    }

                    var result = (object[]) ActiveRecordMediator.FindAll(type, criteria);
                    if (result.Length > 0)
                        instance = result[0];
                }
            }

            if (instance == null && attr.Create) {
                instance = Activator.CreateInstance(type);
            }

            return instance;
        }

        private static PrimaryKeyModel ObtainPrimaryKey(ActiveRecordModel model) {
            if (model.IsJoinedSubClass || model.IsDiscriminatorSubClass) {
                return ObtainPrimaryKey(model.Parent);
            }
            return model.PrimaryKey;
        }

        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
            return FetchActiveRecord(bindingContext.ModelType, bindingContext.ModelName, controllerContext.HttpContext.Request, controllerContext.RouteData.Values);
        }
    }
}